/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@file:JvmMultifileClass
@file:JvmName("RoomDatabaseKt")

package androidx.room

import androidx.annotation.RestrictTo
import androidx.room.concurrent.CloseBarrier
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.room.util.getCoroutineContext
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteException
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.reflect.KClass
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext

/**
 * Base class for all Room databases. All classes that are annotated with [Database] must extend
 * this class.
 *
 * RoomDatabase provides direct access to the underlying database implementation but you should
 * prefer using [Dao] classes.
 *
 * @see Database
 */
public expect abstract class RoomDatabase() {

    /**
     * The invalidation tracker for this database.
     *
     * You can use the invalidation tracker to get notified when certain tables in the database are
     * modified.
     *
     * @return The invalidation tracker for the database.
     */
    public val invalidationTracker: InvalidationTracker

    /**
     * A barrier that prevents the database from closing while the [InvalidationTracker] is using
     * the database asynchronously.
     *
     * @return The barrier for [close].
     */
    internal val closeBarrier: CloseBarrier

    /**
     * Called by Room when it is initialized.
     *
     * @param configuration The database configuration.
     * @throws IllegalArgumentException if initialization fails.
     */
    internal fun init(configuration: DatabaseConfiguration)

    /**
     * Creates a connection manager to manage database connection. Note that this function is called
     * when the [RoomDatabase] is initialized.
     *
     * @param configuration The database configuration
     * @return A new connection manager
     */
    internal fun createConnectionManager(
        configuration: DatabaseConfiguration
    ): RoomConnectionManager

    /**
     * Creates a delegate to configure and initialize the database when it is being opened.
     *
     * An implementation of this function is generated by the Room processor. Note that this
     * function is called when the [RoomDatabase] is initialized.
     *
     * @return A new delegate to be used while opening the database
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) // used in generated code
    protected open fun createOpenDelegate(): RoomOpenDelegateMarker

    /**
     * Creates the invalidation tracker
     *
     * An implementation of this function is generated by the Room processor. Note that this
     * function is called when the [RoomDatabase] is initialized.
     *
     * @return A new invalidation tracker.
     */
    protected abstract fun createInvalidationTracker(): InvalidationTracker

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun getCoroutineScope(): CoroutineScope

    /**
     * Returns a Set of required [AutoMigrationSpec] classes.
     *
     * An implementation of this function is generated by the Room processor. Note that this
     * function is called when the [RoomDatabase] is initialized.
     *
     * @return Creates a set that will include the classes of all required auto migration specs for
     *   this database.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) // used in generated code
    public open fun getRequiredAutoMigrationSpecClasses(): Set<KClass<out AutoMigrationSpec>>

    /**
     * Returns a list of automatic [Migration]s that have been generated.
     *
     * An implementation of this function is generated by the Room processor. Note that this
     * function is called when the [RoomDatabase] is initialized.
     *
     * @param autoMigrationSpecs the provided specs needed by certain migrations.
     * @return A list of migration instances each of which is a generated 'auto migration'.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) // used in generated code
    public open fun createAutoMigrations(
        autoMigrationSpecs: Map<KClass<out AutoMigrationSpec>, AutoMigrationSpec>
    ): List<Migration>

    /**
     * Gets the instance of the given type converter class.
     *
     * This function should only be called by the generated DAO implementations.
     *
     * @param klass The Type Converter class.
     * @param T The type of the expected Type Converter subclass.
     * @return An instance of T.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) // used in generated code
    public fun <T : Any> getTypeConverter(klass: KClass<T>): T

    /**
     * Adds a provided type converter to be used in the database DAOs.
     *
     * @param kclass the class of the type converter
     * @param converter an instance of the converter
     */
    internal fun addTypeConverter(kclass: KClass<*>, converter: Any)

    /**
     * Returns a Map of String -> List&lt;KClass&gt; where each entry has the `key` as the DAO name
     * and `value` as the list of type converter classes that are necessary for the database to
     * function.
     *
     * An implementation of this function is generated by the Room processor. Note that this method
     * is called when the [RoomDatabase] is initialized.
     *
     * @return A map that will include all required type converters for this database.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) // used in generated code
    protected open fun getRequiredTypeConverterClasses(): Map<KClass<*>, List<KClass<*>>>

    /** Property delegate of [getRequiredTypeConverterClasses] for common ext functionality. */
    internal val requiredTypeConverterClassesMap: Map<KClass<*>, List<KClass<*>>>

    /**
     * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
     * initialized and opens a database connection.
     *
     * @param connection The database connection.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) // used in generated code
    protected fun internalInitInvalidationTracker(connection: SQLiteConnection)

    /**
     * Closes the database.
     *
     * Once a [RoomDatabase] is closed it should no longer be used.
     */
    public fun close()

    /**
     * Use a connection to perform database operations.
     *
     * This function is for internal access to the pool, it is an unconfined coroutine function to
     * be used by Room generated code paths and runtime internals and is not marked with
     * [RoomExternalOperationElement]. For the public version see [useReaderConnection] and
     * [useWriterConnection].
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public suspend fun <R> useConnection(isReadOnly: Boolean, block: suspend (Transactor) -> R): R

    /**
     * Journal modes for SQLite database.
     *
     * @see Builder#setJournalMode
     */
    public enum class JournalMode {
        /** Truncate journal mode. */
        TRUNCATE,

        /** Write-Ahead Logging mode. */
        WRITE_AHEAD_LOGGING,
    }

    /**
     * Builder for [RoomDatabase].
     *
     * @param T The type of the abstract database class.
     */
    public class Builder<T : RoomDatabase> {
        /**
         * Sets the [SQLiteDriver] implementation to be used by Room to open database connections.
         *
         * @param driver The driver
         * @return This builder instance.
         */
        public fun setDriver(driver: SQLiteDriver): Builder<T>

        /**
         * Adds a migration to the builder.
         *
         * Each [Migration] has a start and end versions and Room runs these migrations to bring the
         * database to the latest version.
         *
         * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
         * going from version 3 to 5 without going to version 4). If Room opens a database at
         * version 3 and latest version is >= 5, Room will use the migration object that can migrate
         * from 3 to 5 instead of 3 to 4 and 4 to 5.
         *
         * @param migrations The migration objects that modify the database schema with the
         *   necessary changes for a version change.
         * @return This builder instance.
         */
        public fun addMigrations(vararg migrations: Migration): Builder<T>

        /**
         * Adds an auto migration spec instance to the builder.
         *
         * @param autoMigrationSpec The auto migration object that is annotated with
         *   [ProvidedAutoMigrationSpec] and is declared in an [AutoMigration] annotation.
         * @return This builder instance.
         */
        public fun addAutoMigrationSpec(autoMigrationSpec: AutoMigrationSpec): Builder<T>

        /**
         * Allows Room to destructively recreate database tables if [Migration]s that would migrate
         * old database schemas to the latest schema version are not found.
         *
         * When the database version on the device does not match the latest schema version, Room
         * runs necessary [Migration]s on the database. If it cannot find the set of [Migration]s
         * that will bring the database to the current version, it will throw an
         * [IllegalStateException]. You can call this method to change this behavior to re-create
         * the database tables instead of crashing.
         *
         * To let Room fallback to destructive migration only during a schema downgrade then use
         * [fallbackToDestructiveMigrationOnDowngrade].
         *
         * @param dropAllTables Set to `true` if all tables should be dropped during destructive
         *   migration including those not managed by Room. Recommended value is `true` as otherwise
         *   Room could leave obsolete data when table names or existence changes between versions.
         * @return This builder instance.
         */
        public fun fallbackToDestructiveMigration(dropAllTables: Boolean): Builder<T>

        /**
         * Allows Room to destructively recreate database tables if [Migration]s are not available
         * when downgrading to old schema versions.
         *
         * For details, see [Builder.fallbackToDestructiveMigration].
         *
         * @param dropAllTables Set to `true` if all tables should be dropped during destructive
         *   migration including those not managed by Room. Recommended value is `true` as otherwise
         *   Room could leave obsolete data when table names or existence changes between versions.
         * @return This builder instance.
         */
        public fun fallbackToDestructiveMigrationOnDowngrade(dropAllTables: Boolean): Builder<T>

        /**
         * Informs Room that it is allowed to destructively recreate database tables from specific
         * starting schema versions.
         *
         * This functionality is the same [fallbackToDestructiveMigration], except that this method
         * allows the specification of a set of schema versions for which destructive recreation is
         * allowed.
         *
         * Using this method is preferable to [fallbackToDestructiveMigration] if you want to allow
         * destructive migrations from some schema versions while still taking advantage of
         * exceptions being thrown due to unintentionally missing migrations.
         *
         * Note: No versions passed to this method may also exist as either starting or ending
         * versions in the [Migration]s provided via [addMigrations]. If a version passed to this
         * method is found as a starting or ending version in a Migration, an exception will be
         * thrown.
         *
         * @param dropAllTables Set to `true` if all tables should be dropped during destructive
         *   migration including those not managed by Room. Recommended value is `true` as otherwise
         *   Room could leave obsolete data when table names or existence changes between versions.
         * @param startVersions The set of schema versions from which Room should use a destructive
         *   migration.
         * @return This builder instance.
         */
        public fun fallbackToDestructiveMigrationFrom(
            dropAllTables: Boolean,
            vararg startVersions: Int,
        ): Builder<T>

        /**
         * Adds a type converter instance to the builder.
         *
         * @param typeConverter The converter instance that is annotated with
         *   [ProvidedTypeConverter].
         * @return This builder instance.
         */
        public fun addTypeConverter(typeConverter: Any): Builder<T>

        /**
         * Sets the journal mode for this database.
         *
         * The value is ignored if the builder is for an 'in-memory database'. The journal mode
         * should be consistent across multiple instances of [RoomDatabase] for a single SQLite
         * database file.
         *
         * The default value is [JournalMode.WRITE_AHEAD_LOGGING].
         *
         * @param journalMode The journal mode.
         * @return This builder instance.
         */
        public fun setJournalMode(journalMode: JournalMode): Builder<T>

        /**
         * Sets the [CoroutineContext] that will be used to execute all asynchronous queries and
         * tasks, such as `Flow` emissions and [InvalidationTracker] notifications.
         *
         * If no [CoroutineDispatcher] is present in the [context] then this function will throw an
         * [IllegalArgumentException]
         *
         * @param context The context
         * @return This [Builder] instance
         * @throws IllegalArgumentException if the [context] has no [CoroutineDispatcher]
         */
        public fun setQueryCoroutineContext(context: CoroutineContext): Builder<T>

        /**
         * Adds a [Callback] to this database.
         *
         * @param callback The callback.
         * @return This builder instance.
         */
        public fun addCallback(callback: Callback): Builder<T>

        /**
         * Creates the database and initializes it.
         *
         * @return A new database instance.
         * @throws IllegalArgumentException if the builder was misconfigured.
         */
        public fun build(): T
    }

    /**
     * A container to hold migrations. It also allows querying its contents to find migrations
     * between two versions.
     */
    public class MigrationContainer() {
        /**
         * Returns the map of available migrations where the key is the start version of the
         * migration, and the value is a map of (end version -> Migration).
         *
         * @return Map of migrations keyed by the start version
         */
        public fun getMigrations(): Map<Int, Map<Int, Migration>>

        /**
         * Adds the given migrations to the list of available migrations. If 2 migrations have the
         * same start-end versions, the latter migration overrides the previous one.
         *
         * @param migrations List of available migrations.
         */
        public fun addMigrations(migrations: List<Migration>)

        /**
         * Add a [Migration] to the container. If the container already has a migration with the
         * same start-end versions then it will be overwritten.
         *
         * @param migration the migration to add.
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun addMigration(migration: Migration)

        /**
         * Indicates if the given migration is contained within the [MigrationContainer] based on
         * its start-end versions.
         *
         * @param startVersion Start version of the migration.
         * @param endVersion End version of the migration
         * @return True if it contains a migration with the same start-end version, false otherwise.
         */
        public fun contains(startVersion: Int, endVersion: Int): Boolean

        /**
         * Returns a pair corresponding to an entry in the map of available migrations whose key is
         * [migrationStart] and its sorted keys in ascending order.
         */
        internal fun getSortedNodes(migrationStart: Int): Pair<Map<Int, Migration>, Iterable<Int>>?

        /**
         * Returns a pair corresponding to an entry in the map of available migrations whose key is
         * [migrationStart] and its sorted keys in descending order.
         */
        internal fun getSortedDescendingNodes(
            migrationStart: Int
        ): Pair<Map<Int, Migration>, Iterable<Int>>?
    }

    /** Callback for [RoomDatabase] */
    public abstract class Callback() {
        /**
         * Called when the database is created for the first time.
         *
         * This function called after all the tables are created.
         *
         * @param connection The database connection.
         */
        public open fun onCreate(connection: SQLiteConnection)

        /**
         * Called after the database was destructively migrated.
         *
         * @param connection The database connection.
         */
        public open fun onDestructiveMigration(connection: SQLiteConnection)

        /**
         * Called when the database has been opened.
         *
         * @param connection The database connection.
         */
        public open fun onOpen(connection: SQLiteConnection)
    }
}

/**
 * Acquires a READ connection, suspending while waiting if none is available and then calling the
 * [block] to use the connection once it is acquired. A [RoomDatabase] will have one or more READ
 * connections. The connection to use in the [block] is an instance of [Transactor] that provides
 * the capabilities for performing nested transactions.
 *
 * Using the connection after [block] completes is prohibited.
 *
 * The connection will be confined to the coroutine on which [block] executes, attempting to use the
 * connection from a different coroutine will result in an error.
 *
 * If the current coroutine calling this function already has a confined connection, then that
 * connection is used.
 *
 * A connection is a limited resource and should not be held for more than it is needed. The best
 * practice in using connections is to avoid executing long-running computations within the [block].
 * If a caller has to wait too long to acquire a connection a [SQLiteException] will be thrown due
 * to a timeout.
 *
 * @param block The code to use the connection.
 * @throws SQLiteException when the database is closed or a thread confined connection needs to be
 *   upgraded or there is a timeout acquiring a connection.
 * @see [useWriterConnection]
 */
public suspend fun <R> RoomDatabase.useReaderConnection(block: suspend (Transactor) -> R): R =
    withContext(getCoroutineContext(false) + RoomExternalOperationElement) {
        useConnection(isReadOnly = true, block)
    }

/**
 * Acquires a WRITE connection, suspending while waiting if none is available and then calling the
 * [block] to use the connection once it is acquired. A [RoomDatabase] will have only one WRITE
 * connection. The connection to use in the [block] is an instance of [Transactor] that provides the
 * capabilities for performing nested transactions.
 *
 * Using the connection after [block] completes is prohibited.
 *
 * The connection will be confined to the coroutine on which [block] executes, attempting to use the
 * connection from a different coroutine will result in an error.
 *
 * If the current coroutine calling this function already has a confined connection, then that
 * connection is used as long as it isn't required to be upgraded to a writer. If an upgrade is
 * required then a [SQLiteException] is thrown.
 *
 * A connection is a limited resource and should not be held for more than it is needed. The best
 * practice in using connections is to avoid executing long-running computations within the [block].
 * If a caller has to wait too long to acquire a connection a [SQLiteException] will be thrown due
 * to a timeout.
 *
 * @param block The code to use the connection.
 * @throws SQLiteException when the database is closed or a thread confined connection needs to be
 *   upgraded or there is a timeout acquiring a connection.
 * @see [useReaderConnection]
 */
public suspend fun <R> RoomDatabase.useWriterConnection(block: suspend (Transactor) -> R): R =
    withContext(getCoroutineContext(false) + RoomExternalOperationElement) {
            useConnection(isReadOnly = false, block)
        }
        .also { invalidationTracker.refreshAsync() }

/**
 * A coroutine context element to mark coroutines that requested the use of a connection.
 *
 * This marker is used to identify database operations external to Room generated code or runtime
 * internals and is specifically needed to continue coroutines dispatching behaviour before the
 * introduction of the driver APIs and its ability to create suspending transactions.
 */
internal object RoomExternalOperationElement :
    CoroutineContext.Element, CoroutineContext.Key<RoomExternalOperationElement> {
    override val key: CoroutineContext.Key<RoomExternalOperationElement>
        get() = RoomExternalOperationElement
}

/**
 * Validates that no added migration start or end are also marked as fallback to destructive
 * migration from.
 */
internal fun validateMigrationsNotRequired(
    migrationStartAndEndVersions: Set<Int>,
    migrationsNotRequiredFrom: Set<Int>,
) {
    if (migrationStartAndEndVersions.isNotEmpty()) {
        for (version in migrationStartAndEndVersions) {
            require(!migrationsNotRequiredFrom.contains(version)) {
                "Inconsistency detected. A Migration was supplied to addMigration() that has a " +
                    "start or end version equal to a start version supplied to " +
                    "fallbackToDestructiveMigrationFrom(). Start version is: $version"
            }
        }
    }
}

internal fun RoomDatabase.validateAutoMigrations(configuration: DatabaseConfiguration) {
    val autoMigrationSpecs = mutableMapOf<KClass<out AutoMigrationSpec>, AutoMigrationSpec>()
    val requiredAutoMigrationSpecs = getRequiredAutoMigrationSpecClasses()
    val usedSpecs = BooleanArray(configuration.autoMigrationSpecs.size)
    for (spec in requiredAutoMigrationSpecs) {
        var foundIndex = -1
        for (providedIndex in configuration.autoMigrationSpecs.indices.reversed()) {
            val provided: Any = configuration.autoMigrationSpecs[providedIndex]
            if (spec.isInstance(provided)) {
                foundIndex = providedIndex
                usedSpecs[foundIndex] = true
                break
            }
        }
        require(foundIndex >= 0) {
            "A required auto migration spec (${spec.qualifiedName}) is missing in the " +
                "database configuration."
        }
        autoMigrationSpecs[spec] = configuration.autoMigrationSpecs[foundIndex]
    }
    for (providedIndex in configuration.autoMigrationSpecs.indices.reversed()) {
        require(providedIndex < usedSpecs.size && usedSpecs[providedIndex]) {
            "Unexpected auto migration specs found. " +
                "Annotate AutoMigrationSpec implementation with " +
                "@ProvidedAutoMigrationSpec annotation or remove this spec from the " +
                "builder."
        }
    }
    val autoMigrations = createAutoMigrations(autoMigrationSpecs)
    for (autoMigration in autoMigrations) {
        val migrationExists =
            configuration.migrationContainer.contains(
                autoMigration.startVersion,
                autoMigration.endVersion,
            )
        if (!migrationExists) {
            configuration.migrationContainer.addMigration(autoMigration)
        }
    }
}

internal fun RoomDatabase.validateTypeConverters(configuration: DatabaseConfiguration) {
    val requiredFactories = this.requiredTypeConverterClassesMap
    // Indices for each converter on whether it is used or not so that we can throw an exception
    // if developer provides an unused converter. It is not necessarily an error but likely
    // to be because why would developer add a converter if it won't be used?
    val used = BooleanArray(configuration.typeConverters.size)
    requiredFactories.forEach { (daoName, converters) ->
        for (converter in converters) {
            var foundIndex = -1
            // traverse provided converters in reverse so that newer one overrides
            for (providedIndex in configuration.typeConverters.indices.reversed()) {
                val provided = configuration.typeConverters[providedIndex]
                if (converter.isInstance(provided)) {
                    foundIndex = providedIndex
                    used[foundIndex] = true
                    break
                }
            }
            require(foundIndex >= 0) {
                "A required type converter (${converter.qualifiedName}) for" +
                    " ${daoName.qualifiedName} is missing in the database configuration."
            }
            addTypeConverter(converter, configuration.typeConverters[foundIndex])
        }
    }
    // now, make sure all provided factories are used
    for (providedIndex in configuration.typeConverters.indices.reversed()) {
        if (!used[providedIndex]) {
            val converter = configuration.typeConverters[providedIndex]
            throw IllegalArgumentException(
                "Unexpected type converter $converter. " +
                    "Annotate TypeConverter class with @ProvidedTypeConverter annotation " +
                    "or remove this converter from the builder."
            )
        }
    }
}
